package com.ayu.shortlink.admin.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.ayu.shortlink.admin.common.biz.user.UserContext;
import com.ayu.shortlink.admin.common.constant.RedisCacheConstant;
import com.ayu.shortlink.admin.common.convention.exception.ClientException;
import com.ayu.shortlink.admin.common.enums.ChainBizMarkEnum;
import com.ayu.shortlink.admin.common.enums.CouponTemplateStatusEnum;
import com.ayu.shortlink.admin.dao.entity.CouponTemplateDO;
import com.ayu.shortlink.admin.dao.mapper.CouponTemplateMapper;
import com.ayu.shortlink.admin.dto.req.CouponTemplateAddNumReqDTO;
import com.ayu.shortlink.admin.dto.req.CouponTemplateAddReqDTO;
import com.ayu.shortlink.admin.dto.req.CouponTemplatePageQueryReqDTO;
import com.ayu.shortlink.admin.dto.resp.CouponTemplatePageQueryRespDTO;
import com.ayu.shortlink.admin.dto.resp.CouponTemplateQueryRespDTO;
import com.ayu.shortlink.admin.mq.basics.event.CouponDeleteEmptyCacheEvent;
import com.ayu.shortlink.admin.mq.producer.CouponDeleteEmptyCacheProducer;
import com.ayu.shortlink.admin.service.CouponTemplateService;
import com.ayu.shortlink.admin.service.basics.filter.AbstractVerifyFilterChainContext;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 优惠券业务逻辑实现
 */
@Service
@RequiredArgsConstructor
public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper, CouponTemplateDO> implements CouponTemplateService {
    private final CouponTemplateMapper couponTemplateMapper;
    private final AbstractVerifyFilterChainContext<CouponTemplateAddReqDTO> abstractVerifyFilterChainContext;
    private final StringRedisTemplate stringRedisTemplate;
    private final RBloomFilter<String> couponCreateCachePenetrationBloomFilter;
    private final Redisson redisson;
    private final RedissonClient redissonClient;
    private final Integer MAX_STOCK_ADD_LIMIT = 1000000;
    private final CouponDeleteEmptyCacheProducer couponDeleteEmptyCacheProducer;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void createCouponTemplate(CouponTemplateAddReqDTO requestParam) {
        //进行优惠券模版参数校验
        abstractVerifyFilterChainContext.handler(ChainBizMarkEnum.MERCHANT_ADMIN_CREATE_COUPON_TEMPLATE_KEY.name(),requestParam);
        CouponTemplateDO couponTemplateDO = BeanUtil.toBean(requestParam, CouponTemplateDO.class);
        //设置为活跃
        couponTemplateDO.setStatus(CouponTemplateStatusEnum.ACTIVE.getStatus());
        couponTemplateDO.setShopNumber(UserContext.getShopNumber());
        couponTemplateMapper.insert(couponTemplateDO);

        // 将优惠券模版数量存储在Redis中 并设置过期时间
        StringBuilder var = new StringBuilder().
                append(UserContext.getShopNumber()).
                append("_").
                append(couponTemplateDO.getId());
        String couponTemplateCapacityKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);
        long expire = DateUtil.between(new Date(), couponTemplateDO.getValidEndTime(), DateUnit.MS);
        stringRedisTemplate.opsForValue().set(couponTemplateCapacityKey, String.valueOf(couponTemplateDO.getStock()),expire, TimeUnit.MILLISECONDS);

        //添加到布隆过滤器
        String couponTemplateExistKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_EXIST_KEY, var);
        couponCreateCachePenetrationBloomFilter.add(couponTemplateExistKey);
    }

    @Override
    public IPage<CouponTemplatePageQueryRespDTO> pageQueryCouponTemplate(CouponTemplatePageQueryReqDTO requestParam) {
        //构造查询语句
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber,UserContext.getShopNumber())
                .like(StrUtil.isNotBlank(requestParam.getName()), CouponTemplateDO::getName, requestParam.getName())
                .eq(ObjectUtil.isNotEmpty(requestParam.getTarget()), CouponTemplateDO::getTarget, requestParam.getTarget())
                .like(StrUtil.isNotBlank(requestParam.getGoods()), CouponTemplateDO::getGoods, requestParam.getGoods())
                .eq(ObjectUtil.isNotEmpty(requestParam.getType()), CouponTemplateDO::getType, requestParam.getType());

        IPage<CouponTemplateDO> selectPage= couponTemplateMapper.selectPage(requestParam,queryWrapper);
        //进行转换
        return selectPage.convert(item -> BeanUtil.toBean(item,CouponTemplatePageQueryRespDTO.class));
    }

    @Override
    public CouponTemplateQueryRespDTO findCouponTemplateById(String couponTemplateId) {
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber())
                .eq(CouponTemplateDO::getId, couponTemplateId);

        return BeanUtil.toBean(couponTemplateMapper.selectOne(queryWrapper),CouponTemplateQueryRespDTO.class);
    }

    @Override
    public CouponTemplateDO findCouponTemplate(String shopNumber, String couponTemplateId) {
        StringBuilder var = new StringBuilder()
                .append(shopNumber)
                .append("_")
                .append(couponTemplateId);
        String couponTemplateKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_KEY, var);
        //存在布隆过滤器的 KEY
        String couponTemplateExistKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_EXIST_KEY, var);

        if(!couponCreateCachePenetrationBloomFilter.contains(couponTemplateExistKey)){
            throw new ClientException("没有该优惠券模版");
        }
        String jsonStr = stringRedisTemplate.opsForValue().get(couponTemplateKey);
        if(jsonStr != null) {
            //没有该信息
            if(jsonStr.equals("_")){
                throw new ClientException("没有该优惠券模版");
            }
            return JSON.parseObject(jsonStr, CouponTemplateDO.class);
        }

        String key = String.format(RedisCacheConstant.LOCK_FIND_COUPON_TEMPLATE, var);
        RLock lock = redisson.getLock(key);
        lock.lock();
        CouponTemplateDO couponTemplate = null;
        try {
            //双重判定锁
            jsonStr = stringRedisTemplate.opsForValue().get(couponTemplateKey);
            if(jsonStr != null) {
                //没有该信息
                if(jsonStr.equals("_")){
                    throw new ClientException("没有该优惠券模版");
                }
                return JSON.parseObject(jsonStr, CouponTemplateDO.class);
            }

            LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                    .eq(CouponTemplateDO::getShopNumber, Long.parseLong(shopNumber))
                    .eq(CouponTemplateDO::getId, couponTemplateId);

            couponTemplate = couponTemplateMapper.selectOne(queryWrapper);

            if(couponTemplate == null){
                stringRedisTemplate.opsForValue().set(couponTemplateKey, "_");
                throw new ClientException("没有该优惠券模版");
            }else {
                // 设置过期时间为30分钟
                stringRedisTemplate.opsForValue().set(couponTemplateKey, JSON.toJSONString(couponTemplate),30, TimeUnit.MINUTES);
            }
        }finally {
            lock.unlock();
        }

        return couponTemplate;
    }

    @Override
    public void cancelCouponTemplateById(String couponTemplateId) {
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber())
                .eq(CouponTemplateDO::getId, couponTemplateId);
        CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(queryWrapper);
        if(couponTemplateDO != null) {
            couponTemplateDO.setStatus(CouponTemplateStatusEnum.ENDED.getStatus());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addCouponTemplateNumber(CouponTemplateAddNumReqDTO requestParam) {
        if(requestParam == null || StrUtil.isBlank(requestParam.getCouponTemplateId()) || ObjectUtil.isEmpty(requestParam.getStock())){
            throw new ClientException("参数不能为空,请重新输入");
        }
        if(requestParam.getStock()<=0){
            throw new ClientException("请输入正数");
        }
        if(requestParam.getStock()>MAX_STOCK_ADD_LIMIT){
            throw new ClientException("超过添加的最大值");
        }
        StringBuilder var = new StringBuilder()
                .append(requestParam.getShopNumber())
                .append("_")
                .append(requestParam.getCouponTemplateId());
        // 使用分布式读写锁 保证优惠券数量
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(
                String.format(RedisCacheConstant.LOCK_COUPON_TEMPLATE_STOCK_CHANGE, var));

        RLock rLock = readWriteLock.writeLock();
        rLock.lock();
        try {
            String couponTemplateCapacityKey = String.format(
                    RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);
            if(Boolean.FALSE.equals(stringRedisTemplate.hasKey(couponTemplateCapacityKey))){
                throw new ClientException("没有该优惠券");
            }
            //数据库加
            couponTemplateMapper.increaseStock(requestParam.getShopNumber(), requestParam.getCouponTemplateId(), requestParam.getStock());
            //增加
            stringRedisTemplate.opsForValue().increment(couponTemplateCapacityKey, requestParam.getStock());
        }finally {
            rLock.unlock();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addCouponTemplateNumber2(CouponTemplateAddNumReqDTO requestParam) {
        if(requestParam == null || StrUtil.isBlank(requestParam.getCouponTemplateId()) || ObjectUtil.isEmpty(requestParam.getStock())){
            throw new ClientException("参数不能为空,请重新输入");
        }
        if(requestParam.getStock()<=0){
            throw new ClientException("请输入正数");
        }
        if(requestParam.getStock()>MAX_STOCK_ADD_LIMIT){
            throw new ClientException("超过添加的最大值");
        }
        StringBuilder var = new StringBuilder()
                .append(requestParam.getShopNumber())
                .append("_")
                .append(requestParam.getCouponTemplateId());
        String couponTemplateExistKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_EXIST_KEY, var);
        String couponTemplateCapacityKey = String.format(
                    RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);
        // 布隆过滤器判断
        if(!couponCreateCachePenetrationBloomFilter.contains(couponTemplateExistKey)){
                throw new ClientException("没有该优惠券");
        }
        // 数据库加
        couponTemplateMapper.increaseStock(requestParam.getShopNumber(), requestParam.getCouponTemplateId(), requestParam.getStock());

        // Lua脚本逻辑
        String LuaScript =
                "local current_value = redis.call('GET', KEYS[1])" +
                "if current_value == nil or tonumber(current_value) < 0 then" +
                "    redis.call('SET', KEYS[1], ARGV[1])" +
                "else " +
                "    local new_value = tonumber(current_value) + tonumber(ARGV[1])" +
                "    redis.call('SET', KEYS[1], new_value)" +
                "end";
        DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>(LuaScript,Void.class);

        // Tips : 在增加的基础上 多加一些
        stringRedisTemplate.execute(redisScript, List.of(couponTemplateCapacityKey),requestParam.getStock()+10);

        // 同时需要通知各个抢票的服务器去删除库存为空的本地缓存(广播通知)
        CouponDeleteEmptyCacheEvent event = CouponDeleteEmptyCacheEvent.builder()
                .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                .shopNumber(Long.valueOf(requestParam.getShopNumber()))
                .build();

        SendResult sendResult = couponDeleteEmptyCacheProducer.sendMessage(event);

        // TODO：发送不成功的兜底策略
        if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)){
            log.error("");
        }
    }
}
